﻿using Dapper;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace Gaea
{
    internal static partial class GaeaSuperPower
    {
        private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> _keyProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>>();

        private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> _typeProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>>();

        private static readonly ConcurrentDictionary<RuntimeTypeHandle, string> _typeGaeaName = new ConcurrentDictionary<RuntimeTypeHandle, string>();

        private static readonly ConcurrentDictionary<RuntimeTypeHandle, Tuple<string, string>> _findByIdCommandTexts = new ConcurrentDictionary<RuntimeTypeHandle, Tuple<string, string>>();
        private static readonly ConcurrentDictionary<RuntimeTypeHandle, string>
            _insertCommandTexts = new ConcurrentDictionary<RuntimeTypeHandle, string>();
        private static readonly ConcurrentDictionary<RuntimeTypeHandle, string>
    _updateCommandTexts = new ConcurrentDictionary<RuntimeTypeHandle, string>();

        private static IGaeaAdapter _defaultGaeaAdapter;
        private static Dictionary<RuntimeTypeHandle, IGaeaAdapter> _dapterDictionary = new Dictionary<RuntimeTypeHandle, IGaeaAdapter>();

        public static Func<Type, Type, string> GaeaNameBuilder = null;



        private static bool IsNotExcludable(PropertyInfo propertyInfo)
        {
            var attributes = propertyInfo.GetCustomAttributes(typeof(ExcludeAttribute), false);
            if (attributes.Length != 1)
            {
                return true;
            }
            return false;
        }

        private static List<PropertyInfo> TypePropertiesCache(Type type)
        {
            if (_typeProperties.TryGetValue(type.TypeHandle, out IEnumerable<PropertyInfo> pis))
            {
                return pis.ToList();
            }

            var properties = type.GetProperties().Where(IsNotExcludable).ToArray();
            _typeProperties[type.TypeHandle] = properties;
            return properties.ToList();
        }

        private static List<PropertyInfo> KeyPropertiesCache(Type type)
        {
            if (_keyProperties.TryGetValue(type.TypeHandle, out IEnumerable<PropertyInfo> pi))
            {
                return pi.ToList();
            }
            var allProperties = TypePropertiesCache(type);
            var keyProperties = allProperties.Where(lm => lm.GetCustomAttributes(true).Any(ln => ln is KeyAttribute)).ToList();

            if (keyProperties.Count == 0)
            {
                var idProperty = allProperties.Find(lm => string.Equals(lm.Name, "id", StringComparison.CurrentCultureIgnoreCase));
                keyProperties.Add(idProperty);
            }
            _keyProperties[type.TypeHandle] = keyProperties;
            return keyProperties;
        }


        public static string BuildGaeaName<T>(GaeaBasicPower gaeaBasicPower) where T : IGaeaSon
        {
            return BuildGaeaName(gaeaBasicPower, typeof(T));
        }

        public static string BuildGaeaName(GaeaBasicPower gaeaBasicPower, Type typeOfGaeaSon)
        {
            return BuildGaeaName(gaeaBasicPower.GetType(), typeOfGaeaSon);
        }

        public static string BuildGaeaName(Type typeOfGaeaPower, Type typeOfGaeaSon)
        {
            if (_typeGaeaName.TryGetValue(typeOfGaeaSon.TypeHandle, out string name))
            {
                return name;
            }
            if (GaeaNameBuilder != null)
            {
                return GaeaNameBuilder(typeOfGaeaPower, typeOfGaeaSon);
            }
            var gaeaNameAttribute = typeOfGaeaSon.GetCustomAttribute<GaeaNameAttribute>(false);
            if (gaeaNameAttribute != null)
            {
                switch (gaeaNameAttribute.GaeaNamePattern)
                {
                    case GaeaNamePattern.TimeFormat:
                        return DateTime.Now.ToString(gaeaNameAttribute.Name);
                    default:
                        name = gaeaNameAttribute.Name;
                        break;
                }
            }
            else
            {
                name = typeOfGaeaSon.Name;
            }


            var gaeaOptionCache = GaeaExtension.ServiceProviderRoot.GetRequiredService<IGaeaOptionCache>();
            var gaeaOption = gaeaOptionCache.GetGaeaOption(typeOfGaeaPower);
            if (gaeaOption.GaeaNameSymbo != null && gaeaOption.GaeaNameSymbo.Length > 0)
            {
                var left = gaeaOption.GaeaNameSymbo[0];
                var right = gaeaOption.GaeaNameSymbo.Length == 2 ? gaeaOption.GaeaNameSymbo[1] : gaeaOption.GaeaNameSymbo[0];
                name = left + name + right;
            }
            _typeGaeaName[typeOfGaeaSon.TypeHandle] = name;
            return name;
        }

        private static PropertyInfo GetSingleKey<T>(string method)
        {
            var type = typeof(T);
            var keys = KeyPropertiesCache(type);
            if (keys.Count != 1)
            {
                throw new DataException($"{method}<T> only supports an entity with a single [Key] or [Exclude] property");
            }
            return keys[0];
        }

        private static IGaeaAdapter GetGaeaAdapter(this IDbConnection dbConnection)
        {
            var dbConnectionTypeHandle = dbConnection.GetType().TypeHandle;
            return _dapterDictionary.ContainsKey(dbConnectionTypeHandle) ? _dapterDictionary[dbConnectionTypeHandle] : _defaultGaeaAdapter;
        }


        public static void SetCommandBuilder<T>(IGaeaAdapter gaeaAdapter, bool isDefault = false) where T : IDbConnection
        {
            SetCommandBuilder(typeof(T), gaeaAdapter, isDefault);
        }


        public static void SetCommandBuilder(Type typeOfbConnection, IGaeaAdapter gaeaAdapter, bool isDefault = false)
        {
            if (isDefault)
            {
                _defaultGaeaAdapter = gaeaAdapter;
            }
            if (!_dapterDictionary.ContainsKey(typeOfbConnection.TypeHandle))
            {
                _dapterDictionary.Add(typeOfbConnection.TypeHandle, gaeaAdapter);
            }
        }

        public static async Task<T> FindByIdAsync<T>(this IDbConnection dbConnection, GaeaBasicPower gaeaBasicPower, dynamic id, IDbTransaction transaction = null, int? commandTimeout = null) where T : class
        {
            var typeOfGaeaSon = typeof(T);
            if (!_findByIdCommandTexts.TryGetValue(typeOfGaeaSon.TypeHandle, out Tuple<string, string> commandTextInfo))
            {
                var gaeaName = BuildGaeaName(gaeaBasicPower, typeOfGaeaSon);
                var key = GetSingleKey<T>(nameof(FindByIdAsync));
                var sqlDapter = dbConnection.GetGaeaAdapter();
                var commandText = sqlDapter.FindByIdCommandText(out string paramName, gaeaName, key.Name);

                commandTextInfo = new Tuple<string, string>(commandText, paramName);
                _findByIdCommandTexts[typeOfGaeaSon.TypeHandle] = commandTextInfo;
            }

            var dynamicParameters = new DynamicParameters();

            dynamicParameters.Add(commandTextInfo.Item2, id);

            T gaeaSon;
            if (typeOfGaeaSon.IsInterface)
            {
                var queryResult = await dbConnection.QuerySingleOrDefaultAsync(commandTextInfo.Item1, dynamicParameters) as IDictionary<string, object>;
                if (queryResult == null)
                {
                    return null;
                }

                gaeaSon = ProxyGenerator.GetInterfaceProxy<T>();

                foreach (var property in TypePropertiesCache(typeOfGaeaSon))
                {
                    var val = queryResult[property.Name];
                    if (val == null) continue;
                    if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                    {
                        var genericType = Nullable.GetUnderlyingType(property.PropertyType);
                        if (genericType != null) property.SetValue(gaeaSon, Convert.ChangeType(val, genericType), null);
                    }
                    else
                    {
                        property.SetValue(gaeaSon, Convert.ChangeType(val, property.PropertyType), null);
                    }
                }
                ((IProxy)gaeaSon).IsDirty = false;
            }
            else
            {
                gaeaSon = await dbConnection.QuerySingleOrDefaultAsync<T>(commandTextInfo.Item1, dynamicParameters, transaction, commandTimeout: commandTimeout);
            }
            return gaeaSon;
        }

        public static Task<IEnumerable<T>> FindAllAsync<T>(this IDbConnection dbConnection, GaeaBasicPower gaeaBasicPower, IDbTransaction transaction = null, int? commandTimeout = null) where T : class
        {
            var typeOfGaeaSon = typeof(T);
            var gaeaAdapter = dbConnection.GetGaeaAdapter();
            var gaeaName = BuildGaeaName(gaeaBasicPower, typeOfGaeaSon);
            var commandText = gaeaAdapter.FindAllCommandText(gaeaName);
            return dbConnection.QueryAsync<T>(commandText, null, transaction, commandTimeout);
        }

        public static Task<int> InsertAsync<T>(this IDbConnection dbConnection, GaeaBasicPower gaeaBasicPower, T entityToInsert, IDbTransaction transaction = null, int? commandTimeout = null) where T : class
        {
            var typeOfGaeaSon = typeof(T);
            var isArray = false;
            if (typeOfGaeaSon.IsArray)
            {
                isArray = true;
                typeOfGaeaSon = typeOfGaeaSon.GetElementType();
            }
            else if (typeOfGaeaSon.IsGenericType)
            {
                var implementsGenericIEnumerableOrIsGenericIEnumerable = typeOfGaeaSon.GenericTypeArguments.Any(lm => lm.IsGenericType && lm.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || typeOfGaeaSon.GetGenericTypeDefinition() == typeof(IEnumerable<>);

                if (implementsGenericIEnumerableOrIsGenericIEnumerable)
                {
                    isArray = true;
                    typeOfGaeaSon = typeOfGaeaSon.GetGenericArguments()[0];
                }
            }

            var gaeaAdapter = dbConnection.GetGaeaAdapter();
            if (!_insertCommandTexts.TryGetValue(typeOfGaeaSon.TypeHandle, out string commandText))
            {
                var gaeaName = BuildGaeaName(gaeaBasicPower, typeOfGaeaSon);
                var allProperties = TypePropertiesCache(typeOfGaeaSon);
                var keyProperties = KeyPropertiesCache(typeOfGaeaSon);

                var allPropertiesExceptKey = allProperties.Except(keyProperties).ToList();


                commandText = gaeaAdapter.InsertCommandText(gaeaName, allPropertiesExceptKey);
                _insertCommandTexts.TryAdd(typeOfGaeaSon.TypeHandle, commandText);
            }
            if (!isArray)
            {
                return gaeaAdapter.InsertAsync(dbConnection, transaction, commandText, entityToInsert, commandTimeout);
            }
            else
            {
                return dbConnection.ExecuteAsync(commandText, entityToInsert, transaction, commandTimeout);
            }

        }


        public static async Task<int> UpdateAsync<T>(this IDbConnection dbConnection, GaeaBasicPower gaeaBasicPower, T entityToUpdate, IDbTransaction transaction = null, int? commandTimeout = null)
        {
            var typeOfGaeaSon = typeof(T);
            if (typeOfGaeaSon.IsArray)
            {
                typeOfGaeaSon = typeOfGaeaSon.GetElementType();
            }
            else if (typeOfGaeaSon.IsGenericType)
            {
                var implementsGenericIEnumerableOrIsGenericIEnumerable = typeOfGaeaSon.GenericTypeArguments.Any(lm => lm.IsGenericType && lm.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || typeOfGaeaSon.GetGenericTypeDefinition() == typeof(IEnumerable<>);

                if (implementsGenericIEnumerableOrIsGenericIEnumerable)
                {
                    typeOfGaeaSon = typeOfGaeaSon.GetGenericArguments()[0];
                }
            }

            var gaeaAdapter = dbConnection.GetGaeaAdapter();
            if (!_updateCommandTexts.TryGetValue(typeOfGaeaSon.TypeHandle, out string commandText))
            {
                var gaeaName = BuildGaeaName(gaeaBasicPower, typeOfGaeaSon);
                var allProperties = TypePropertiesCache(typeOfGaeaSon);
                var keyProperties = KeyPropertiesCache(typeOfGaeaSon);

                var allPropertiesExceptKey = allProperties.Except(keyProperties).ToList();

                commandText = gaeaAdapter.UpdateCommandText(gaeaName, allPropertiesExceptKey, keyProperties);
                _updateCommandTexts.TryAdd(typeOfGaeaSon.TypeHandle, commandText);
            }
            return await dbConnection.ExecuteAsync(commandText.ToString(), entityToUpdate, commandTimeout: commandTimeout, transaction: transaction).ConfigureAwait(false);
        }

        public static async Task<int> DeleteAsync<T>(this IDbConnection dbConnection, GaeaBasicPower gaeaBasicPower, T entityToDelete, IDbTransaction transaction = null, int? commandTimeout = null)
        {
            if (entityToDelete == null)
            {
                throw new ArgumentException("Cannot delete null object", nameof(entityToDelete));
            }
            var typeOfGaeaSon = typeof(T);
            if (typeOfGaeaSon.IsArray)
            {
                typeOfGaeaSon = typeOfGaeaSon.GetElementType();
            }
            else if (typeOfGaeaSon.IsGenericType)
            {
                var implementsGenericIEnumerableOrIsGenericIEnumerable = typeOfGaeaSon.GenericTypeArguments.Any(lm => lm.IsGenericType && lm.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || typeOfGaeaSon.GetGenericTypeDefinition() == typeof(IEnumerable<>);

                if (implementsGenericIEnumerableOrIsGenericIEnumerable)
                {
                    typeOfGaeaSon = typeOfGaeaSon.GetGenericArguments()[0];
                }
            }
            var gaeaAdapter = dbConnection.GetGaeaAdapter();
            var gaeaName = BuildGaeaName(gaeaBasicPower, typeOfGaeaSon);
            var keyProperties = KeyPropertiesCache(typeOfGaeaSon);
            var commandText = gaeaAdapter.DeleteCommandText(gaeaName, keyProperties);
            return await dbConnection.ExecuteAsync(commandText, entityToDelete, transaction, commandTimeout).ConfigureAwait(false);
        }

        public static async Task<int> DeleteAllAsync<T>(this IDbConnection dbConnection, GaeaBasicPower gaeaBasicPower, IDbTransaction transaction = null, int? commandTimeout = null, bool keyRestart = false)
        {
            var typeOfGaeaSon = typeof(T);
            var gaeaAdapter = dbConnection.GetGaeaAdapter();
            var gaeaName = BuildGaeaName(gaeaBasicPower, typeOfGaeaSon);
            var commandText = gaeaAdapter.DeleteAllCommandText(gaeaName, keyRestart);
            return await dbConnection.ExecuteAsync(commandText, null, transaction, commandTimeout).ConfigureAwait(false);
        }
    }
}
